AWS Lambdaを利用したAutoScaling DynamoDB
はじめに
Amazon DynamoDBは低レイテンシで安定したスループットもつNoSQLサービスです。コストが低く使いやすいので弊社でも様々な案件で利用しています。 DynamoDBではあらかじめテーブルごとに読み込み/書き込みのスループットを指定することができます。 例えば、参照が多いサービスで利用する場合は読み込み:1000回/秒、書き込み:100回/秒のプロビジョンドスループットを設定するという具合です。
プロビジョンドスループットを超えた操作が発生した場合、バーストキャパシティ(バースト用に貯められている予備キャパシティ)が残っていればそれが利用されますが、バーストキャパシティを使い切っている場合、操作に失敗します。 なので、ある程度余裕を持ったプロビジョンドスループットの設定が必要です。
とはいってもスループットを事前に予測するのは難しいため、予測を超えたスループットが発生した場合に自動でプロビジョンドスループットを増加させるツールがあります。 例えば、以前本ブログでも紹介したDynamic DynamoDBです。 Dynamic DynamoDB を使ってみた 今回はAWS Lambdaを利用することでEC2無しで同様の機能を実現したいと思います。
概要
DynamoDBのCloudWatchメトリクスにSNSアラームを設定し、SNSアラームを受け取ったLambdaでスループットを増加させます。 スループット増加に合わせてアラームの閾値も上げるようにしました。
アラーム発生時のスループット増加の割合や、新しい閾値の値の算出基準はLambdaファンクション内で指定しました。 DynamoDBテーブルにタグが付けられるようになったら、タグで指定するようにしたいです。
手順
DynamoDBアラームの設定
DynamoDBテーブル作成後、「Alarm Setup」タグからアラームの設定をしてください。 今回はプロビジョンドスループットは読み込み/書き込み共に1、消費スループットが設置値の80%を超えたらアラームが飛ぶようにしています。
CloudWatchアラームの画面で確認すると、閾値は240になっています。 スループットは秒間1で設定しているので5分間の合計キャパシティは300、その80%で240が閾値になっています。
Lambdaファンクションの作成
アラームの条件を満たすとSNS経由でイベントが送られます。イベント内に記載されているメッセージは次のようなものになります。
{ "AlarmName":"scaling-demo-ReadCapacityUnitsLimit-BasicAlarm", "AlarmDescription":null, "AWSAccountId":"xxxxxxxxxxxxx", "NewStateValue":"ALARM", "NewStateReason":"Threshold Crossed: 1 datapoint (241.5) was greater than or equal to the threshold (240.0).", "StateChangeTime":"2015-07-07T22:32:57.068+0000", "Region":"APAC - Tokyo", "OldStateValue":"OK", "Trigger": { "MetricName":"ConsumedReadCapacityUnits", "Namespace":"AWS/DynamoDB", "Statistic":"SUM", "Unit":null, "Dimensions":[ { "name":"TableName", "value":"scaling-demo" } ], "Period":300, "EvaluationPeriods":1, "ComparisonOperator":"GreaterThanOrEqualToThreshold", "Threshold":240.0 } }
下記のスクリプトでDynamoDBテーブルのプロビジョンドスループットとCloudWatchアラームの閾値を変更するようにしました。 アラームが届いた場合は、スループットを20%増やし、アラームの閾値は新スループットの80%にするようにしています。
var AWS = require('aws-sdk'); var async = require('async'); var increaseReadPercentage = 20; var increaseWritePercentage = 20; var readAlarmThreshold = 80; var writeAlarmThreshold = 80; var region = { 'APAC - Tokyo': 'ap-northeast-1', '...': '...' }; exports.handler = function(event, context) { var message = JSON.parse(event.Records[0].Sns.Message) ; if (message.NewStateValue =! 'ALARM') context.succeed(message); AWS.config.update({ region: region[message.Region]}); async.waterfall( [ // modify throughput function (callback) { var dynamodb = new AWS.DynamoDB(); var dynamodbTable = message.Trigger.Dimensions[0].value; dynamodb.describeTable({TableName: dynamodbTable}, function(err, tableInfo) { if (err) callback(err); else { var params = { TableName: dynamodbTable } var currentThroughput = tableInfo.Table.ProvisionedThroughput; if (message.Trigger.MetricName == 'ConsumedReadCapacityUnits') { params.ProvisionedThroughput = { ReadCapacityUnits: Math.ceil(currentThroughput.ReadCapacityUnits * (100 + increaseReadPercentage)/100), WriteCapacityUnits: currentThroughput.WriteCapacityUnits }; } else if (message.Trigger.MetricName == 'ConsumedWriteCapacityUnits') { params.ProvisionedThroughput = { ReadCapacityUnits: currentThroughput.ReadCapacityUnits, WriteCapacityUnits: Math.ceil(currentThroughput.WriteCapacityUnits * (100 + increaseWritePercentage)/100) }; } else { callback(message); } dynamodb.updateTable(params, function(err, response) { if (err) callback(err); else { console.log('modify provisioned throughput:', currentThroughput, params); callback(null, params.ProvisionedThroughput); } }); } }); }, // modify CloudWatch Alarm function (newThroughput, callback) { var cloudwatch = new AWS.CloudWatch(); cloudwatch.describeAlarms({AlarmNames: [message.AlarmName]}, function(err, alarms) { if (err) callback(err); else { var currentAlarm = alarms.MetricAlarms[0]; var params = { AlarmName: currentAlarm.AlarmName, ComparisonOperator: currentAlarm.ComparisonOperator, EvaluationPeriods: currentAlarm.EvaluationPeriods, MetricName: currentAlarm.MetricName, Namespace: currentAlarm.Namespace, Period: currentAlarm.Period, Statistic: currentAlarm.Statistic, ActionsEnabled: currentAlarm.ActionsEnabled, AlarmActions: currentAlarm.AlarmActions, AlarmDescription: currentAlarm.AlarmDescription, Dimensions: currentAlarm.Dimensions, InsufficientDataActions: currentAlarm.InsufficientDataActions, OKActions: currentAlarm.OKActions, Unit: currentAlarm.Unit, } if (message.Trigger.MetricName == 'ConsumedReadCapacityUnits') { params.Threshold = newThroughput.ReadCapacityUnits * params.Period * readAlarmThreshold / 100; } else { params.Threshold = newThroughput.WriteCapacityUnits * params.Period * writeAlarmThreshold / 100; } cloudwatch.putMetricAlarm(params, function(err, response) { if (err) callback(err); else { console.log('modify alarm:', currentAlarm, params); callback(null, response); } }); } }); } ], function (err, result) { if (err) {context.fail(err);} else {context.succeed(result);} } ); }
コードはこちらに上げています。
Lambda用IAM Roleの作成
Lambdaから
- DynamoDBテーブルのスループット変更
- CloudWatchアラームの閾値変更
を行うので、通常のLambdaの実行権限(CloudWatch Logsへのログ出力権限)に上記権限を追加したIAM Roleを作成します。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1436323246000", "Effect": "Allow", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "*" ] }, { "Sid": "Stmt1436323279000", "Effect": "Allow", "Action": [ "cloudwatch:DescribeAlarms", "cloudwatch:PutMetricAlarm" ], "Resource": [ "*" ] }, { "Sid": "Stmt1436323303000", "Effect": "Allow", "Action": [ "dynamodb:DescribeTable", "dynamodb:UpdateTable" ], "Resource": [ "*" ] } ] }
作成したRoleは次のようになります。
$ aws iam get-role --role-name lambda_dynamodb_alarm_role { "Role": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "lambda.amazonaws.com" }, "Effect": "Allow", "Sid": "" } ] }, "RoleId": "AROAJ7565HM7GVB742NOU", "CreateDate": "2015-07-08T02:39:29Z", "RoleName": "lambda_dynamodb_alarm_role", "Path": "/", "Arn": "arn:aws:iam::{{your-account-id}}:role/lambda_dynamodb_alarm_role" } } $ aws iam get-role-policy --role-name lambda_dynamodb_alarm_role --policy-name policygen-lambda_dynamodb_alarm_role-201507071942 { "RoleName": "lambda_dynamodb_alarm_role", "PolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "*" ], "Effect": "Allow", "Sid": "Stmt1436323246000" }, { "Action": [ "cloudwatch:DescribeAlarms", "cloudwatch:PutMetricAlarm" ], "Resource": [ "*" ], "Effect": "Allow", "Sid": "Stmt1436323279000" }, { "Action": [ "dynamodb:DescribeTable", "dynamodb:UpdateTable" ], "Resource": [ "*" ], "Effect": "Allow", "Sid": "Stmt1436323303000" } ] }, "PolicyName": "policygen-lambda_dynamodb_alarm_role-201507071942" }
Lambdaファンクションの登録
こちらからコードをcloneし、登録します。
$ git clone https://github.com/yokota-shinsuke/aws-lambda-scaling-dynamodb $ cd aws-lambda-scaling-dynamodb/ $ npm install $ zip -r scalingDynamoDB.zip scalingDynamoDB.js node_modules/ $ aws lambda create-function --function-name scalingDynamoDB --runtime nodejs --role arn:aws:iam::{{your-account-id}}:role/lambda_dynamodb_alarm_role --handler "scalingDynamoDB.handler" --timeout 60 --zip-file "fileb://scalingDynamoDB.zip" --region ap-northeast-1
イベントソースの設定
Lambdaの管理画面からDynamoDBアラーム用のSNSトピックをイベントソースとして設定します。
動作確認
実際にDynamoDBテーブルにプロビジョンドスループットを超えるアクセスをしてみます。
$ while true; do aws dynamodb get-item --table-name scaling-demo --key '{"id": {"S": "test001"}}' 2>&1; done
しばらくすると、CloudWatchLogsにLambdaからのログ出力がありました。 DynamoDBテーブルやCloudWatchアラームの設定を見ると期待通りに変更されていました。
もともと、Read Throughputは1だったのですが、20%増加(切り上げ)で2に変更されています。 もともと、閾値は1スループット/秒 × 300秒 × 80% = 240だったのですが、スループットが2になったことで480にあがりました。